package mylittleinvestigations;

import mylittleinvestigations.CaseInformation.Case;
import mylittleinvestigations.CaseInformation.CaseParseException;
import mylittleinvestigations.GameUi.Tab;
import mylittleinvestigations.GameUi.TabRow;
import mylittleinvestigations.GameUi.Arrow;
import mylittleinvestigations.GameUi.ArrowDirection;
import java.util.*;
import mylittleinvestigations.Events.DialogEventProvider;
import mylittleinvestigations.Events.EvidenceSelectorEventProvider;
import mylittleinvestigations.Events.EvidenceSelectorEventListener;
import mylittleinvestigations.PersistentThreads.RenderThread;
import org.dawnmew.jpurewave.SoundSystem;
import org.newdawn.slick.*;
import org.newdawn.slick.geom.*;

public class Dialog implements EvidenceSelectorEventListener
{
    public static final int Height = 180; // px
    
    private static final double DefaultMillisecondsPerCharacterUpdate = 33;
    private static final double FullStopMillisecondPause = 500;
    private static final double HalfStopMillisecondPause = 250;
    private static final double EllipsisMillisecondPause = 150;

    private static final Color NormalTextColor = Color.white;
    private static final Color AsideTextColor = new Color(0xFF87CEEB); //Color.SkyBlue from C#
    
    private static final String LetterBlipSoundEffect = "LetterBlip";

    private static Rectangle textAreaRect;
    private static float desiredPadding;
    private static UnicodeFont dialogFont;

    private static Arrow interrogationUpArrow;
    private static Arrow interrogationDownArrow;

    private int curTextPosition;

    private double millisecondsPerCharacterUpdate;
    private double millisecondsSinceLastUpdate;
    private double millisecondsUntilPauseCompletes;
    private double millisecondsUntilAudioPauseCompletes;
    
    private ArrayList<DialogEvent> dialogEventList;
    private Iterator<DialogEvent> dialogEventIterator;
    private DialogEvent currentDialogEvent;

    private ArrayList<Interval> asideIntervalList;
    private int asideStartIndex;

    private double timeSinceLetterBlipPlayed;

    private String filePath;
    private int delayBeforeContinuing;
    private boolean isInterrogation;
    private boolean isPassive;

    private Tab characterNameTab;
    private Tab pressForInfoTab;
    private Tab presentEvidenceTab;
    private Partner currentPartner;
    private Tab usePartnerTab;
    private Tab endInterrogationTab;

    private Arrow conversationDownArrow;

    private boolean canNavigateBack;
    private boolean canNavigateForward;

    private EvidenceSelector evidenceSelector;
    private boolean presentEvidenceAutomatically;
    private boolean evidencePresented;
    private boolean endRequested;
    private boolean evidenceSelectorShownOnce;

    private Dialog(String filePath, int delayBeforeContinuing, boolean isInterrogation, boolean isPassive, boolean isConfrontation, boolean canNavigateBack, boolean canNavigateForward, boolean presentEvidenceAutomatically)
    {
        this.curTextPosition = 0;

        this.millisecondsSinceLastUpdate = 0;
        this.millisecondsUntilPauseCompletes = 0;
        this.millisecondsUntilAudioPauseCompletes = 0;
        
        this.dialogEventList = new ArrayList<DialogEvent>();
        this.dialogEventIterator = null;
        this.currentDialogEvent = null;

        this.asideIntervalList = new ArrayList<Interval>();
        this.asideStartIndex = -1;
        
        this.timeSinceLetterBlipPlayed = Double.MAX_VALUE;

        this.filePath = filePath;
        this.delayBeforeContinuing = delayBeforeContinuing;
        this.isInterrogation = isInterrogation;
        this.isPassive = isPassive;

        this.characterNameTab = new Tab(0, false /* isClickable */);
        this.pressForInfoTab = new Tab((int)Settings.DisplayBounds.getX() - 3 * Tab.Width - 14, true /* isClickable */, "PRESS FOR INFO");
        this.presentEvidenceTab = new Tab((int)Settings.DisplayBounds.getX() - 2 * Tab.Width - 7, true /* isClickable */, "PRESENT EVIDENCE");
        this.usePartnerTab = new Tab((int)Settings.DisplayBounds.getX() / 2 - Tab.Width / 2, true /* isClickable */, "", false /* useCancelClickSoundEffect */, TabRow.Top);
        this.endInterrogationTab = new Tab((int)Settings.DisplayBounds.getX() - Tab.Width, true /* isClickable */, isConfrontation ? "BACK" : "END INTERROGATION");

        this.conversationDownArrow = new Arrow(927, 505, ArrowDirection.Down, 10 /* bounceDistance */, false /* isClickable */);

        this.canNavigateBack = canNavigateBack;
        this.canNavigateForward = canNavigateForward;

        this.evidenceSelector = new EvidenceSelector(true /* !presentEvidenceAutomatically */);
        this.presentEvidenceAutomatically = presentEvidenceAutomatically;
    }

    public static void Initialize(
        int textAreaLeft,
        int textAreaTop,
        int textAreaWidth,
        int textAreaHeight,
        float desiredPadding,
        UnicodeFont dialogFont)
    {
        Dialog.textAreaRect = new Rectangle(textAreaLeft, textAreaTop, textAreaWidth, textAreaHeight);
        Dialog.desiredPadding = desiredPadding;
        Dialog.dialogFont = dialogFont;

        Dialog.interrogationUpArrow = new Arrow(927, 375, ArrowDirection.Up, 10 /* bounceDistance */, true /* isClickable */);
        Dialog.interrogationDownArrow = new Arrow(927, 505, ArrowDirection.Down, 10 /* bounceDistance */, true /* isClickable */);
    }

    public static Dialog CreateForString(String speakerName, String dialogText, String filePath, int delayBeforeContinuing, boolean isInterrogation, boolean isPassive, boolean isConfrontation, boolean canNavigateBack, boolean canNavigateForward, boolean presentEvidenceAutomatically) throws CaseParseException
    {
        Dialog dialog = new Dialog(filePath, delayBeforeContinuing, isInterrogation, isPassive, isConfrontation, canNavigateBack, canNavigateForward, presentEvidenceAutomatically);

        dialog.characterNameTab.setText(speakerName);

        double allowedWidth = textAreaRect.getWidth() - desiredPadding * 2;
        String fullString = "";
        List<String> wordList = new ArrayList<String>(Arrays.asList(dialogText.split(" ")));

        while (wordList.size() > 0)
        {
            String curString = "";
            double curTextWidth = 0;
            boolean lineDone = false;
            boolean addSpace = false;

            if (!Utils.isNullOrEmpty(fullString))
            {
                fullString += "\n";
            }

            while (!lineDone)
            {
                String stringToTest = (addSpace ? " " : "") + wordList.get(0);
                double curStringWidth = dialogFont.getWidth(dialog.StripEvents(stringToTest));

                if (curTextWidth + curStringWidth < allowedWidth)
                {
                    StringByRefParameter stringToPrependOnNext = new StringByRefParameter();
                    stringToTest = dialog.ParseEvents(fullString.length() + curString.length(), stringToTest, stringToPrependOnNext);
                    curString += stringToTest;
                    curTextWidth += curStringWidth;
                    wordList.remove(0);
                    addSpace = true;

                    if (wordList.isEmpty())
                    {
                        lineDone = true;
                    }
                    else
                    {
                        wordList.set(0, stringToPrependOnNext.Value + wordList.get(0));
                    }
                }
                else
                {
                    lineDone = true;
                }
            }

            fullString += curString;
        }

        dialog.setText(fullString);
        
        if (delayBeforeContinuing >= 0)
        {
            dialog.AddPausePosition(fullString.length(), delayBeforeContinuing);
        }
        
        return dialog;
    }

    private String text;

    public String getText() { return this.text; }
    public void setText(String text) { this.text = text; }

    public boolean getIsStarted()
    {
        return this.curTextPosition > 0;
    }

    public boolean getIsPaused()
    {
        return this.millisecondsUntilPauseCompletes > 0;
    }
    
    public boolean getIsAudioPaused()
    {
        return
            this.millisecondsUntilAudioPauseCompletes > 0 ||
            (this.millisecondsUntilPauseCompletes > 0 && !Settings.PlayDialogAudio);
    }

    public boolean getIsFinished()
    {
        return this.curTextPosition == this.getText().length() && this.currentDialogEvent == null && !this.getIsPaused();
    }

    public boolean getIsReadyToHide()
    {
        return !this.evidenceSelector.getIsShowing() && this.endRequested;
    }

    public boolean getIsAutomatic()
    {
        return this.delayBeforeContinuing >= 0;
    }

    public String StripEvents(String stringToStrip)
    {
        String strippedString = stringToStrip;

        while (strippedString.indexOf('{') >= 0 && strippedString.indexOf('}') >= 0)
        {
            int eventStart = strippedString.indexOf('{');
            String[] eventComponents = strippedString.substring(strippedString.indexOf('{') + 1, strippedString.indexOf('}')).split(":");
            String replacementText = "";
            String testString = eventComponents[0].toLowerCase();

            if (Utils.equals(testString, "fullstop"))
            {
                if (eventComponents.length > 1)
                {
                    replacementText = eventComponents[1];
                }
                else
                {
                    replacementText = ".";
                }
            }
            else if (Utils.equals(testString, "halfstop"))
            {
                if (eventComponents.length > 1)
                {
                    replacementText = eventComponents[1];
                }
                else
                {
                    replacementText = ",";
                }
            }
            else if (Utils.equals(testString, "ellipsis"))
            {
                replacementText = "...";
            }

            strippedString = strippedString.substring(0, strippedString.indexOf('{')) + replacementText + strippedString.substring(strippedString.indexOf('}') + 1);
        }

        return strippedString;
    }

    private String ParseEvents(int lineOffset, String stringToParse, StringByRefParameter stringToPrependOnNext) throws CaseParseException
    {
        String parsedString = stringToParse;
        stringToPrependOnNext.Value = "";

        while (parsedString.indexOf('{') >= 0 && parsedString.indexOf('}') >= 0)
        {
            int eventStart = parsedString.indexOf('{');
            int eventEnd = parsedString.indexOf('}');

            String[] eventComponents = parsedString.substring(eventStart + 1, eventEnd).split(":");
            String replacementText = "";
            String testString = eventComponents[0].toLowerCase();

            if (Utils.equals(testString, "speed"))
            {
                double newMillisecondsPerCharacterUpdate = Double.parseDouble(eventComponents[1]);
                this.AddSpeedChangePosition(lineOffset + eventStart, newMillisecondsPerCharacterUpdate);
            }
            else if (Utils.equals(testString, "emotion"))
            {
                String newEmotion = eventComponents[1];
                this.AddEmotionChangePosition(lineOffset + eventStart, newEmotion);
            }
            else if (Utils.equals(testString, "otheremotion"))
            {
                String newOtherEmotion = eventComponents[1];
                this.AddEmotionOtherChangePosition(lineOffset + eventStart, newOtherEmotion);
            }
            else if (Utils.equals(testString, "pause"))
            {
                double millisecondDuration = Double.parseDouble(eventComponents[1]);
                
                // If we're playing vocals, then we'll add 300 milliseconds of audio pause
                // to the end of every lengthy pause.
                // Otherwise, we'll cap pauses at 500 ms, since there's
                // no audio to sync the text to in that case.
                // The exception is automatic dialogs - these may be timed
                // to background music, so we'll leave them exactly as is.
                if (this.getIsAutomatic())
                {
                    this.AddPausePosition(lineOffset + eventStart, millisecondDuration);
                }
                else if (Settings.PlayDialogAudio)
                {
                    if (millisecondDuration > 500)
                    {
                        this.AddPausePosition(lineOffset + eventStart, millisecondDuration - 300);
                        this.AddAudioPausePosition(lineOffset + eventStart, 300);
                    }
                    else
                    {
                        this.AddPausePosition(lineOffset + eventStart, millisecondDuration * 3 / 5);
                        this.AddAudioPausePosition(lineOffset + eventStart, millisecondDuration * 2 / 5);
                    }
                }
                else
                {
                    this.AddPausePosition(lineOffset + eventStart, Math.min(millisecondDuration, 500));
                }
            }
            else if (Utils.equals(testString, "audiopause"))
            {
                double millisecondDuration = Double.parseDouble(eventComponents[1]);
                this.AddAudioPausePosition(lineOffset + eventStart, millisecondDuration);
            }
            else if (Utils.equals(testString, "mouth"))
            {
                boolean mouthIsOn = Utils.equals(eventComponents[1].toLowerCase(), "on");
                this.AddMouthChangePosition(lineOffset + eventStart, mouthIsOn);
            }
            else if (Utils.equals(testString, "fullstop"))
            {
                if (eventComponents.length > 1)
                {
                    replacementText = eventComponents[1];
                }
                else
                {
                    replacementText = ".";
                }

                this.AddMouthChangePosition(lineOffset + eventStart, false /* mouthIsOn */);
                this.AddPausePosition(lineOffset + eventStart + 1, FullStopMillisecondPause);

                if (eventEnd + 1 == parsedString.length())
                {
                    stringToPrependOnNext.Value = "{Mouth:On}";
                }
                else
                {
                    this.AddMouthChangePosition(lineOffset + eventStart + 1, true /* mouthIsOn */);
                }
            }
            else if (Utils.equals(testString, "halfstop"))
            {
                if (eventComponents.length > 1)
                {
                    replacementText = eventComponents[1];
                }
                else
                {
                    replacementText = ",";
                }

                this.AddMouthChangePosition(lineOffset + eventStart, false /* mouthIsOn */);
                this.AddPausePosition(lineOffset + eventStart + 1, HalfStopMillisecondPause);

                if (eventEnd + 1 == parsedString.length())
                {
                    stringToPrependOnNext.Value = "{Mouth:On}";
                }
                else
                {
                    this.AddMouthChangePosition(lineOffset + eventStart + 1, true /* mouthIsOn */);
                }
            }
            else if (Utils.equals(testString, "ellipsis"))
            {
                replacementText = "...";

                this.AddMouthChangePosition(lineOffset + eventStart, false /* mouthIsOn */);
                this.AddPausePosition(lineOffset + eventStart + 1, EllipsisMillisecondPause);
                this.AddPausePosition(lineOffset + eventStart + 2, EllipsisMillisecondPause);
                this.AddPausePosition(lineOffset + eventStart + 3, EllipsisMillisecondPause);

                if (eventEnd + 1 == parsedString.length())
                {
                    stringToPrependOnNext.Value = "{Mouth:On}";
                }
                else
                {
                    this.AddMouthChangePosition(lineOffset + eventStart + 3, true /* mouthIsOn */);
                }
            }
            else if (Utils.equals(testString, "aside"))
            {
                if (this.asideStartIndex == -1)
                {
                    this.asideStartIndex = lineOffset + eventStart;
                }
            }
            else if (Utils.equals(testString, "/aside"))
            {
                if (this.asideStartIndex != -1)
                {
                    this.AddMouthChangePosition(this.asideStartIndex, false /* mouthIsOn */);

                    if (eventEnd + 1 == parsedString.length())
                    {
                        stringToPrependOnNext.Value = "{Mouth:On}";
                    }
                    else
                    {
                        this.AddMouthChangePosition(lineOffset + eventStart, true /* mouthIsOn */);
                    }

                    this.asideIntervalList.add(new Interval(this.asideStartIndex, lineOffset + eventStart));
                    this.asideStartIndex = -1;
                }
            }
            else if (Utils.equals(testString, "playsound"))
            {
                String soundId = eventComponents[1];
                this.AddPlaySoundPosition(lineOffset + eventStart, soundId);
            }
            else if (Utils.equals(testString, "shake"))
            {
                this.AddShakePosition(lineOffset + eventStart);
            }
            else if (Utils.equals(testString, "nextframe"))
            {
                this.AddNextFramePosition(lineOffset + eventStart);
            }
            else if (Utils.equals(testString, "damageplayer"))
            {
                this.AddPlayerDamagedPosition(lineOffset + eventStart);
            }
            else if (Utils.equals(testString, "damageopponent"))
            {
                this.AddOpponentDamagedPosition(lineOffset + eventStart);
            }
            else
            {
                throw new CaseParseException("Unknown event: '" + eventComponents[0] + "'");
            }

            parsedString = parsedString.substring(0, eventStart) + replacementText + parsedString.substring(eventEnd + 1);
        }

        return parsedString;
    }
    
    public void AddSpeedChangePosition(int position, double newMillisecondsPerCharacterUpdate)
    {
        this.dialogEventList.add(new SpeedChangeEvent(position, this, newMillisecondsPerCharacterUpdate));
    }

    public void AddEmotionChangePosition(int position, String newEmotionId)
    {
        this.dialogEventList.add(new SpeakerEmotionChangeEvent(position, this, newEmotionId));
    }

    public void AddEmotionOtherChangePosition(int position, String newEmotionId)
    {
        this.dialogEventList.add(new OtherEmotionChangeEvent(position, this, newEmotionId));
    }

    public void AddPausePosition(int position, double millisecondDuration)
    {
        this.dialogEventList.add(new PauseEvent(position, this, millisecondDuration));
    }

    public void AddAudioPausePosition(int position, double millisecondDuration)
    {
        this.dialogEventList.add(new AudioPauseEvent(position, this, millisecondDuration));
    }

    public void AddMouthChangePosition(int position, boolean mouthIsOn)
    {
        this.dialogEventList.add(new MouthChangeEvent(position, this, mouthIsOn));
    }

    public void AddPlaySoundPosition(int position, String id)
    {
        this.dialogEventList.add(new PlaySoundEvent(position, this, id));
    }

    public void AddShakePosition(int position)
    {
        this.dialogEventList.add(new ShakeEvent(position, this));
    }

    public void AddNextFramePosition(int position)
    {
        this.dialogEventList.add(new NextFrameEvent(position, this));
    }

    public void AddPlayerDamagedPosition(int position)
    {
        this.dialogEventList.add(new PlayerDamagedEvent(position, this));
    }

    public void AddOpponentDamagedPosition(int position)
    {
        this.dialogEventList.add(new OpponentDamagedEvent(position, this));
    }

    public String GetString()
    {
        return this.getIsStarted() ? this.getText().substring(0, this.curTextPosition) : "";
    }

    public void Begin()
    {
        interrogationUpArrow.setIsEnabled(this.canNavigateBack);
        interrogationDownArrow.setIsEnabled(this.canNavigateForward);

        this.evidenceSelectorShownOnce = false;
        this.evidenceSelector.Begin();
        EvidenceSelectorEventProvider.RegisterListener(this);

        if (Case.getInstance().getPartnerManager().GetCurrentPartnerId() != null)
        {
            this.currentPartner = Case.getInstance().getPartnerManager().GetCurrentPartner();

            if (this.currentPartner.getConversationAbilityName() != null)
            {
                this.usePartnerTab.setText(this.currentPartner.getConversationAbilityName());
                this.usePartnerTab.setTabImage(this.currentPartner.getIconSprite());
            }
        }
        
        if (this.filePath != null && Settings.PlayDialogAudio)
        {
            SoundSystem.getInstance().playDialog(this.filePath + ".ogg");
        }
        
        this.millisecondsPerCharacterUpdate = DefaultMillisecondsPerCharacterUpdate;
        this.endRequested = false;
    }

    public void Update(GameContainer gc,int delta)
    {
        boolean newCharacterDrawn = false;
        
        if (this.dialogEventIterator == null && !this.dialogEventList.isEmpty())
        {
            Collections.sort(this.dialogEventList, new DialogEventComparator());
            this.dialogEventIterator = this.dialogEventList.iterator();
            this.currentDialogEvent = this.dialogEventIterator.next();
        }

        if (!this.getIsPaused() && this.currentDialogEvent != null)
        {
            while (this.curTextPosition >= this.currentDialogEvent.getPosition())
            {
                this.currentDialogEvent.RaiseEvent();

                if (this.dialogEventIterator.hasNext())
                {
                    this.currentDialogEvent = this.dialogEventIterator.next();
                }
                else
                {
                    this.currentDialogEvent = null;
                }
                
                if (this.getIsPaused() || this.currentDialogEvent == null)
                {
                    break;
                }
            }
        }

        if (!this.getIsPaused())
        {
            if (!this.getIsFinished())
            {
                this.millisecondsSinceLastUpdate += delta;

                if (!this.getIsStarted() || this.millisecondsSinceLastUpdate > this.millisecondsPerCharacterUpdate)
                {
                    int positionsToAdvance = (int)(this.millisecondsSinceLastUpdate / this.millisecondsPerCharacterUpdate);
                    this.curTextPosition = Math.min(this.curTextPosition + positionsToAdvance, this.getText().length());
                    this.millisecondsSinceLastUpdate = Math.max(this.millisecondsSinceLastUpdate - positionsToAdvance * this.millisecondsPerCharacterUpdate, 0);
                    newCharacterDrawn = true;
                }
            }
        }
        else
        {
            this.millisecondsSinceLastUpdate = 0;
            this.millisecondsUntilPauseCompletes -= delta;
            this.millisecondsUntilAudioPauseCompletes -= delta;

            if (this.millisecondsUntilPauseCompletes < 0)
            {
                this.millisecondsUntilPauseCompletes = 0;
            }

            if (this.millisecondsUntilAudioPauseCompletes < 0)
            {
                this.millisecondsUntilAudioPauseCompletes = 0;
            }
        }

        // We'll only play the letter blips if voice acting isn't enabled.
        if (!Settings.PlayDialogAudio)
        {
            this.timeSinceLetterBlipPlayed += delta;

            if (newCharacterDrawn && this.timeSinceLetterBlipPlayed > 50)// TODO this.letterBlipSoundEffect.Duration.TotalMilliseconds)
            {
                SoundSystem.getInstance().playSound(LetterBlipSoundEffect);
                this.timeSinceLetterBlipPlayed = 0;
            }
        }

        if (this.getIsFinished())
        {
            if (this.presentEvidenceAutomatically && !this.evidenceSelector.getIsShowing())
            {
                if (!this.evidenceSelectorShownOnce)
                {
                    this.evidenceSelector.Show();
                    this.evidenceSelectorShownOnce = true;
                }
                else
                {
                    if (!this.evidencePresented)
                    {
                        // If the evidence selector isn't showing and it was shown once,
                        // and if no evidence has been shown, then the player must've
                        // clicked Cancel, so we'll end the conversation here
                        // since progress depends on presenting the right evidence.
                        this.OnEndRequested();
                    }
                    
                    this.endRequested = true;
                }
            }
            else if (!this.presentEvidenceAutomatically)
            {
                this.endRequested = true;
            }

            if (this.isInterrogation)
            {
                if (this.evidenceSelector.getIsShowing())
                {
                    this.evidenceSelector.Update(gc,delta);
                }
                else
                {
                    if (!this.isPassive)
                    {
                        this.pressForInfoTab.Update();
                        this.presentEvidenceTab.Update();

                        if (Case.getInstance().getPartnerManager().GetCurrentPartnerId() != null && this.currentPartner.getConversationAbilityName() != null)
                        {
                            this.usePartnerTab.Update();
                        }
                    }

                    this.endInterrogationTab.Update();

                    interrogationUpArrow.Update(gc,delta);
                    interrogationDownArrow.Update(gc,delta);
                }
            }
            else
            {
                if (this.evidenceSelector.getIsShowing())
                {
                    this.evidenceSelector.Update(gc,delta);
                }
                else if (!this.getIsAutomatic())
                {
                    this.conversationDownArrow.Update(gc,delta);
                }
            }
        }

        if (this.isInterrogation)
        {
            if (!this.isPassive)
            {
                this.pressForInfoTab.setIsEnabled(this.getIsFinished());
                this.presentEvidenceTab.setIsEnabled(this.getIsFinished() && Case.getInstance().getEvidenceManager().getHasEvidence());

                if (Case.getInstance().getPartnerManager().GetCurrentPartnerId() != null && this.currentPartner.getConversationAbilityName() != null)
                {
                    this.usePartnerTab.setIsEnabled(this.getIsFinished());
                }
            }
            
            this.endInterrogationTab.setIsEnabled(this.getIsFinished());
        }
    }

    public boolean HandleClick()
    {
        // If this dialog is automatic, then we'll ignore every click.
        if (this.getIsAutomatic())
        {
            return false;
        }
        
        if (this.isInterrogation && !this.evidenceSelector.getIsShowing())
        {
            if (!this.isPassive)
            {
                this.pressForInfoTab.Update();
                this.presentEvidenceTab.Update();

                if (Case.getInstance().getPartnerManager().GetCurrentPartnerId() != null && this.currentPartner.getConversationAbilityName() != null)
                {
                    this.usePartnerTab.Update();
                }
            }
            
            this.endInterrogationTab.Update();

            interrogationUpArrow.UpdateState();
            interrogationDownArrow.UpdateState();
        }
        else if (this.evidenceSelector.getIsShowing())
        {
            this.evidenceSelector.UpdateState();
            return true;
        }
        else if (this.presentEvidenceAutomatically && !this.evidenceSelector.getIsShowing() && this.getIsFinished())
        {
            // If the evidence selector is being shown automatically, is not presently showing,
            // and if the dialog is finished, then the user must've canceled.
            // We'll ignore clicks in this case, since we're about to exit.
            return true;
        }

        if (!this.getIsFinished())
        {
            this.Finish();
            return true;
        }
        else if (
            this.pressForInfoTab.getIsClicked() ||
            this.presentEvidenceTab.getIsClicked() ||
            this.usePartnerTab.getIsClicked() ||
            this.endInterrogationTab.getIsClicked() ||
            interrogationUpArrow.getIsClicked() ||
            interrogationDownArrow.getIsClicked())
        {
            if (this.pressForInfoTab.getIsClicked())
            {
                this.OnPressForInfoClicked();
            }
            else if (this.presentEvidenceTab.getIsClicked())
            {
                this.evidenceSelector.Show();
            }
            else if (this.usePartnerTab.getIsClicked())
            {
                this.OnUsePartnerClicked();
            }
            else if (this.endInterrogationTab.getIsClicked())
            {
                this.OnEndRequested();
            }
            else if (interrogationUpArrow.getIsClicked())
            {
                this.OnDirectlyNavigated(DirectNavigationDirection.Back);
            }
            else if (interrogationDownArrow.getIsClicked())
            {
                this.OnDirectlyNavigated(DirectNavigationDirection.Forward);
            }

            return true;
        }

        return false;
    }

    public void Draw(GameContainer gc, Graphics g, float yOffset) throws SlickException
    {
        this.characterNameTab.Draw(gc, g, yOffset);

        if (this.isInterrogation)
        {
            if (this.evidenceSelector.getIsShowing())
            {
                this.evidenceSelector.Draw(gc, g, yOffset);
            }
            else if (!this.evidencePresented)
            {
                if (!this.isPassive)
                {
                    this.pressForInfoTab.Draw(gc, g, yOffset);
                    this.presentEvidenceTab.Draw(gc, g, yOffset);

                    if (Case.getInstance().getPartnerManager().GetCurrentPartnerId() != null && this.currentPartner.getConversationAbilityName() != null)
                    {
                        this.usePartnerTab.Draw(gc, g, yOffset);
                    }
                }

                this.endInterrogationTab.Draw(gc, g, yOffset);
            }
        }
        else if (this.evidenceSelector.getIsShowing())
        {
            this.evidenceSelector.Draw(gc, g, yOffset);
        }

        if (this.getIsStarted())
        {
            Vector2 lineScreenPosition = new Vector2(textAreaRect.getX() + desiredPadding, textAreaRect.getY() + desiredPadding);
            int curTextPosition = 0;
            boolean isInAside = false;
            String[] lines = this.GetString().split("\n");
            Iterator<Interval> asideIntervalEnumerator = null;
            Interval currentAsideInterval = null;

            asideIntervalEnumerator = this.asideIntervalList.iterator();
            
            if (asideIntervalEnumerator.hasNext())
            {
                currentAsideInterval = asideIntervalEnumerator.next();
            }

            for (int i = 0; i < lines.length; i++)
            {
                String line = lines[i];
                int curLineTextPosition = 0;
                Vector2 curScreenPosition = new Vector2(lineScreenPosition.getX(), lineScreenPosition.getY());
                String lineRemainder = line;

                while (
                    (isInAside && currentAsideInterval != null && currentAsideInterval.EndIndex - curTextPosition - curLineTextPosition <= lineRemainder.length()) ||
                    (!isInAside && currentAsideInterval != null && currentAsideInterval.StartIndex - curTextPosition - curLineTextPosition <= lineRemainder.length()))
                {
                    if (isInAside)
                    {
                        String linePortionToDraw = lineRemainder.substring(0, currentAsideInterval.EndIndex - curTextPosition - curLineTextPosition);
                        lineRemainder = lineRemainder.substring(currentAsideInterval.EndIndex - curTextPosition - curLineTextPosition);
                        RenderThread.getInstance().AddStringToRenderQueue(dialogFont, (float)curScreenPosition.getX(), (float)curScreenPosition.getY() + yOffset, linePortionToDraw, AsideTextColor);
                        curScreenPosition.setX(curScreenPosition.getX() + dialogFont.getWidth(linePortionToDraw));
                        curLineTextPosition += linePortionToDraw.length();
                        
                        if (asideIntervalEnumerator.hasNext())
                        {
                            currentAsideInterval = asideIntervalEnumerator.next();
                        }
                        else
                        {
                            currentAsideInterval = null;
                        }
            
                        isInAside = false;
                    }
                    else
                    {
                        String linePortionToDraw = lineRemainder.substring(0, currentAsideInterval.StartIndex - curTextPosition - curLineTextPosition);
                        lineRemainder = lineRemainder.substring(currentAsideInterval.StartIndex - curTextPosition - curLineTextPosition);
                        RenderThread.getInstance().AddStringToRenderQueue(dialogFont, (float)curScreenPosition.getX(), (float)curScreenPosition.getY() + yOffset, linePortionToDraw, NormalTextColor);
                        curScreenPosition.setX(curScreenPosition.getX() + dialogFont.getWidth(linePortionToDraw));
                        curLineTextPosition += linePortionToDraw.length();
                        isInAside = true;
                    }
                }

                RenderThread.getInstance().AddStringToRenderQueue(dialogFont, (float)curScreenPosition.getX(), (float)curScreenPosition.getY() + yOffset, lineRemainder, isInAside ? AsideTextColor : NormalTextColor);
                lineScreenPosition.setY(lineScreenPosition.getY() + dialogFont.getLineHeight());

                // Add one for the carriage return character.
                curTextPosition += line.length() + 1;
            }
        }

        if (this.getIsFinished() && !this.evidencePresented)
        {
            if (this.isInterrogation)
            {
                if (!this.evidenceSelector.getIsShowing())
                {
                    interrogationUpArrow.Draw(gc, g, yOffset);
                    interrogationDownArrow.Draw(gc, g, yOffset);
                }
            }
            else if (!this.evidenceSelector.getIsShowing() && !this.getIsAutomatic())
            {
                this.conversationDownArrow.Draw(gc, g, yOffset);
            }
        }
    }

    public void Finish()
    {
        if (this.dialogEventIterator == null && !this.dialogEventList.isEmpty())
        {
            Collections.sort(this.dialogEventList, new DialogEventComparator());
            this.dialogEventIterator = this.dialogEventList.iterator();
            this.currentDialogEvent = this.dialogEventIterator.next();
        }

        this.curTextPosition = this.getText().length();

        if (this.currentDialogEvent != null)
        {
            while (this.curTextPosition >= this.currentDialogEvent.getPosition())
            {
                if (this.currentDialogEvent.getShouldBeRaisedWhenFinishing())
                {
                    this.currentDialogEvent.RaiseEvent();
                }

                if (this.dialogEventIterator.hasNext())
                {
                    this.currentDialogEvent = this.dialogEventIterator.next();
                }
                else
                {
                    this.currentDialogEvent = null;
                }

                if (this.currentDialogEvent == null)
                {
                    break;
                }
            }
        }
        
        SoundSystem.getInstance().stopDialog();
        this.millisecondsUntilPauseCompletes = 0;
        this.millisecondsUntilAudioPauseCompletes = 0;
    }

    public void Reset()
    {
        this.curTextPosition = 0;
        this.millisecondsUntilPauseCompletes = 0;
        this.millisecondsUntilAudioPauseCompletes = 0;
        this.dialogEventIterator = null;
        this.currentDialogEvent = null;
        this.timeSinceLetterBlipPlayed = Double.MAX_VALUE;
        this.asideStartIndex = -1;

        this.pressForInfoTab.Reset();
        this.presentEvidenceTab.Reset();
        this.currentPartner = null;
        this.usePartnerTab.Reset();
        this.endInterrogationTab.Reset();

        this.conversationDownArrow.Reset();
        interrogationUpArrow.Reset();
        interrogationDownArrow.Reset();

        this.evidenceSelector.Reset();
        EvidenceSelectorEventProvider.ClearListener(this);
        this.evidencePresented = false;
    }

    public void OnEvidenceSelectorEvidencePresented(EvidenceSelector sender, String evidenceId)
    {
        if (sender == this.evidenceSelector)
        {
            this.evidencePresented = true;
            this.OnEvidencePresented(evidenceId);
        }
    }

    private void OnDirectlyNavigated(DirectNavigationDirection direction)
    {
        DialogEventProvider.RaiseDirectlyNavigated(this, direction);
    }

    private void OnPressForInfoClicked()
    {
        DialogEventProvider.RaisePressForInfoClicked(this);
    }

    private void OnEvidencePresented(String evidenceId)
    {
        DialogEventProvider.RaiseEvidencePresented(this, evidenceId);
    }

    private void OnUsePartnerClicked()
    {
        DialogEventProvider.RaiseUsePartner(this);
    }

    private void OnEndRequested()
    {
        DialogEventProvider.RaiseEndRequested(this);
    }

    private class Interval
    {
        public int StartIndex;
        public int EndIndex;

        public Interval(int startIndex, int endIndex)
        {
            this.StartIndex = startIndex;
            this.EndIndex = endIndex;
        }
    }
    
    private abstract class DialogEvent
    {
        private int position;
        protected Dialog owningDialog;
        
        public DialogEvent(int position, Dialog owningDialog)
        {
            this.position = position;
            this.owningDialog = owningDialog;
        }
        
        public int getPosition() { return this.position; }
        
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return false;
        }
        
        public abstract void RaiseEvent();
    }
    
    private class DialogEventComparator implements Comparator<DialogEvent>
    {
        public int compare(DialogEvent dialogEvent1, DialogEvent dialogEvent2)
        {
            return dialogEvent1.getPosition() - dialogEvent2.getPosition();
        }
    }
    
    private class SpeedChangeEvent extends DialogEvent
    {
        double newMillisecondsPerCharacterUpdate;
        
        public SpeedChangeEvent(int position, Dialog owningDialog, double newMillisecondsPerCharacterUpdate)
        {
            super(position, owningDialog);
            
            this.newMillisecondsPerCharacterUpdate = newMillisecondsPerCharacterUpdate;
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            this.owningDialog.millisecondsPerCharacterUpdate = this.newMillisecondsPerCharacterUpdate;
        }
    }
    
    private class SpeakerEmotionChangeEvent extends DialogEvent
    {
        String newEmotionId;
        
        public SpeakerEmotionChangeEvent(int position, Dialog owningDialog, String newEmotionId)
        {
            super(position, owningDialog);
            
            this.newEmotionId = newEmotionId;
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaiseSpeakerEmotionChanged(this.owningDialog, this.newEmotionId);
        }
    }
    
    private class OtherEmotionChangeEvent extends DialogEvent
    {
        String newEmotionId;
        
        public OtherEmotionChangeEvent(int position, Dialog owningDialog, String newEmotionId)
        {
            super(position, owningDialog);
            
            this.newEmotionId = newEmotionId;
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaiseOtherEmotionChanged(this.owningDialog, this.newEmotionId);
        }
    }
    
    private class PauseEvent extends DialogEvent
    {
        double msDuration;
        
        public PauseEvent(int position, Dialog owningDialog, double msDuration)
        {
            super(position, owningDialog);
            
            this.msDuration = msDuration;
        }
        
        public void RaiseEvent()
        {
            this.owningDialog.millisecondsUntilPauseCompletes += this.msDuration;
        }
    }
    
    private class AudioPauseEvent extends DialogEvent
    {
        double msDuration;
        
        public AudioPauseEvent(int position, Dialog owningDialog, double msDuration)
        {
            super(position, owningDialog);
            
            this.msDuration = msDuration;
        }
        
        public void RaiseEvent()
        {
            this.owningDialog.millisecondsUntilPauseCompletes += this.msDuration;
            this.owningDialog.millisecondsUntilAudioPauseCompletes += this.msDuration;
        }
    }
    
    private class MouthChangeEvent extends DialogEvent
    {
        boolean isMouthOpen;
        
        public MouthChangeEvent(int position, Dialog owningDialog, boolean isMouthOpen)
        {
            super(position, owningDialog);
            
            this.isMouthOpen = isMouthOpen;
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaiseSpeakerMouthStateChanged(this.owningDialog, this.isMouthOpen);
        }
    }
    
    private class PlaySoundEvent extends DialogEvent
    {
        String soundId;
        
        public PlaySoundEvent(int position, Dialog owningDialog, String soundId)
        {
            super(position, owningDialog);
            
            this.soundId = soundId;
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            SoundSystem.getInstance().playSound(this.soundId);
        }
    }
    
    private class ShakeEvent extends DialogEvent
    {
        public ShakeEvent(int position, Dialog owningDialog)
        {
            super(position, owningDialog);
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaiseSpeakerStartedShaking(this.owningDialog);
        }
    }
    
    private class NextFrameEvent extends DialogEvent
    {
        public NextFrameEvent(int position, Dialog owningDialog)
        {
            super(position, owningDialog);
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaiseNextFrame(this.owningDialog);
        }
    }
    
    private class PlayerDamagedEvent extends DialogEvent
    {
        public PlayerDamagedEvent(int position, Dialog owningDialog)
        {
            super(position, owningDialog);
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaisePlayerDamaged(this.owningDialog);
        }
    }
    
    private class OpponentDamagedEvent extends DialogEvent
    {
        public OpponentDamagedEvent(int position, Dialog owningDialog)
        {
            super(position, owningDialog);
        }
        
        @Override
        public boolean getShouldBeRaisedWhenFinishing()
        {
            return true;
        }
        
        public void RaiseEvent()
        {
            DialogEventProvider.RaiseOpponentDamaged(this.owningDialog);
        }
    }
    
    private static class StringByRefParameter
    {
        public String Value;
        
        public StringByRefParameter()
        {
            this.Value = null;
        }
    }
}
